﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.DirectoryServices.ActiveDirectory;
using System.DirectoryServices.Protocols;
using System.Net;
using System.Security.Principal;
using System.Threading.Tasks;

using ADSync2.ConfigurationComponent;
using ADSync2.EngineComponents;

namespace ADSync2.ActiveDirectory
{
	/// <summary>
	/// Class delivers set of AD operations
	/// Translates attributes, retrieves multi-valued attributes
	/// and executes change requests
	/// </summary>
	public class Operations : IDisposable
	{
		#region Constants
		private const string SYNC_METADATA_VALUE = "msDS-ReplValueMetaData";
		private const string USER_CLASS_FILTER = "(sAMAccountType=805306368)";
		private const string USER_ENABLED_CLASS_FILTER = "(&(!userAccountControl:1.2.840.113556.1.4.803:=2)(sAMAccountType=805306368))";
		private const string GROUP_CLASS_FILTER = "(&(objectCategory=group)(objectClass=group))";

		#endregion

		#region Properties
		private bool bDisposed;

		private ExecutionModes ExecutionMode
		{
			get;
			set;
		}

		private String LegacyForest;
		private String GlobalDomain;

		private Jobs JobsToExecute
		{
			get;
			set;
		}
		private iLog LogManager
		{
			get;
			set;
		}

		public SyncHistory SynchronizationHistory
		{
			get;
			set;
		}
		#endregion

		#region Constructors
		public Operations(ExecutionModes ExecutionMode, iLog.LoggingModes LoggingMode)
		{
			if ((ExecutionMode & ExecutionModes.FullSync) == ExecutionModes.FullSync)
			{
				this.SynchronizationHistory = new SyncHistory();
			}
			else
			{
				this.SynchronizationHistory = SyncHistory.ReadSynchronizationHistory();
			}
			this.ExecutionMode = ExecutionMode;
			this.JobsToExecute = new Jobs();

			this.LogManager = new iLog(LoggingMode);
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Method retrieves all changed legacy groups
		/// That were migrated under respective Global OU
		/// </summary>
		/// <param name="ObjectDistinguishedName"></param>
		/// <param name="AttributeName"></param>
		/// <returns></returns>
		public void ExecuteJobsLegacyBased(Dictionary<String, String> ConfigurationKeyVal)
		{
			try
			{
				this.LegacyForest = ConfigurationKeyVal["LEGACY_FOREST"];
				this.GlobalDomain = ConfigurationKeyVal["GLOBAL_DOMAIN"];

				DirectoryContext GlobalDomain = new DirectoryContext(DirectoryContextType.Domain, this.GlobalDomain);

				this.LogManager.WriteLog(new iLogEntry("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%", 0, iLogEntry.LogEntrySeverity.Normal));
				this.LogManager.WriteLog(new iLogEntry(String.Format("Engine started ({0}).", GlobalDomain.Name), 2001, iLogEntry.LogEntrySeverity.Normal));
	
				SyncServer SynchronizationServerGlobal = this.GetPreferredGC(Domain.GetDomain(GlobalDomain), double.Parse(ConfigurationKeyVal["INIT_FACTOR"]) );

				//Write global Synchronization State
				this.SynchronizationHistory.UpdateSyncHistory(GlobalDomain.Name, SynchronizationServerGlobal);

				using (Ldap GlobalLdapObject = new Ldap(SynchronizationServerGlobal.ServerFqdn, null, null, null, 389))
				{
					this.LogManager.WriteLog(new iLogEntry(String.Format("Started asynchronously processing all Legacy domains in %{0}% mode.",
								(this.ExecutionMode & ExecutionModes.NoCommit)==ExecutionModes.NoCommit
								 ? "NoCommit"
								 : "Normal" ), 2004, iLogEntry.LogEntrySeverity.Normal));

					//Start enumeration through legacy domains
					Parallel.ForEach(this.GetForestDomains(this.LegacyForest, false), (ForestDomain) =>
					{
						try
						{
							//Load Legacy based Jobs against Global
							int JobsLoaded = this.LoadJobsLegacyBased(ForestDomain, GlobalLdapObject, ConfigurationKeyVal["GROUP_FILTER"], ConfigurationKeyVal["USER_FILTER"], ConfigurationKeyVal["GROUP_BASEDN"], double.Parse(ConfigurationKeyVal["INIT_FACTOR"]));

							this.LogManager.WriteLog(new iLogEntry(String.Format("Having {0} jobs to process for domain {1}.", JobsLoaded, ForestDomain.Name), 2005, iLogEntry.LogEntrySeverity.Normal));
						}
						catch (Exception eX)
						{
							this.LogManager.WriteLog(new iLogEntry(String.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message), 3051, iLogEntry.LogEntrySeverity.Error));
						}
					});

					if (this.JobsToExecute.JobsQueue.Count > 0)
					{
						this.LogManager.WriteLog(new iLogEntry(String.Format("Started processing jobs against {0}.", GlobalDomain.Name), 2006, iLogEntry.LogEntrySeverity.Normal));

						//Execute Legacy based Jobs against Global
						this.ExecuteJobs(GlobalLdapObject);
					}

					if ((ExecutionMode & ExecutionModes.FullSync) != ExecutionModes.FullSync)
					{
						this.LogManager.WriteLog(new iLogEntry(String.Format("Writing synchronization history."), 2006, iLogEntry.LogEntrySeverity.Normal));

						//Write Synchronization History back to file
						this.SynchronizationHistory.WriteSynchronizationHistory();
					}

					this.LogManager.WriteLog(new iLogEntry(String.Format("Engine stopped ({0}).", GlobalDomain.Name), 2011, iLogEntry.LogEntrySeverity.Normal));
				}
			}
			catch (Exception eX)
			{
				throw new Exception(string.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message));
			}
		}

		#endregion

		#region Private Methods
		/// <summary>
		/// Executes Membership Jobs
		/// </summary>
		private void ExecuteJobs(Ldap LdapObject)
		{
			try
			{
				Parallel.ForEach(this.JobsToExecute.JobsQueue, (MembershipChange) =>
				{
					try
					{
						this.LogManager.WriteLog(new iLogEntry(
							String.Format("{0}-operation:user:{1}\t-\tgroup:{2}\t%{3}%.",
								MembershipChange.Value.Action,
								MembershipChange.Value.MemberDistinguishedName, 
								MembershipChange.Key,
								(this.ExecutionMode & ExecutionModes.NoCommit)==ExecutionModes.NoCommit
								 ? "NoCommit" 
								 : "Normal"),
							2050,
							iLogEntry.LogEntrySeverity.Verbose));

						this.CommitMembershipChange(
							LdapObject.LdapConnectionObject,
							MembershipChange.Key,
							MembershipChange.Value.MemberDistinguishedName,
							MembershipChange.Value.Action == MembershipAction.Operation.REM,
							(this.ExecutionMode & ExecutionModes.NoCommit) != ExecutionModes.NoCommit
						);
					}
					catch (Exception eX)
					{
						using (EventLog EventLogObject = new System.Diagnostics.EventLog("Application", ".", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name))
						{
							this.LogManager.WriteLog(new iLogEntry(
								String.Format("{0}-operation:user:{1}\t-\tgroup:{2}:failed:{3}",
									MembershipChange.Value.Action,
									MembershipChange.Value.MemberDistinguishedName,
									MembershipChange.Key,
									String.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message)
									),
								2050,
								iLogEntry.LogEntrySeverity.Warning));
						}
					}
				}
				);
			}
			catch (Exception eX)
			{
				throw new Exception(string.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message));
			}

		}

		/// <summary>
		/// Method retrieves translated single and multi-valued attribute value(s)
		/// Designed for sidHistory, tokenGroups, member and memberOf
		/// May support other attributes although it's not its primary function.
		/// </summary>
		/// <param name="ObjectDistinguishedName"></param>
		/// <param name="AttributeName"></param>
		/// <returns></returns>
		private HashSet<String> GetAttributeValues(String ObjectDistinguishedName, String AttributeName)
		{
			return new HashSet<string>();
		}

		/// <summary>
		/// Returns domains from given forest.
		/// </summary>
		/// <param name="ForestFqdn"></param>
		/// <returns></returns>
		private IEnumerable<Domain> GetForestDomains(String ForestFqdn, bool IncludeRootDomain)
		{
			DirectoryContext ForestContext;
			try
			{
				ForestContext = new DirectoryContext(DirectoryContextType.Forest, ForestFqdn);
			}
			catch (Exception eX)
			{
				throw new Exception(string.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message));
			}

			using (Forest ForestObject = Forest.GetForest(ForestContext))
			{
				foreach (Domain ForestDomain in ForestObject.Domains)
				{
					if(IncludeRootDomain || !	ForestDomain.Name.Equals(ForestFqdn, StringComparison.OrdinalIgnoreCase))
					{
						yield return ForestDomain;
					}
				}
			}
		}

		/// <summary>
		/// Method retrieves GC server for synchronization
		/// for given domain and serverFqdn/serverGuid
		/// </summary>
		/// <param name="DirectoryContextType"></param>
		/// <param name="PreferedServers"></param>
		/// <returns></returns>
		private SyncServer GetPreferredGC(Domain ForestDomain, double InitializationFactor)
		{
			try
			{
				GlobalCatalog FoundServer = null;

				//Add try recommended GC from config file then hit for sync metadata


				//check if server in synchronization history
				if (this.SynchronizationHistory.DomainSyncStates.ContainsKey(ForestDomain.Name))
				{
					try
					{
						DirectoryContext ServerContext = new DirectoryContext(DirectoryContextType.DirectoryServer, this.SynchronizationHistory.DomainSyncStates[ForestDomain.Name].ServerFqdn);
						FoundServer = GlobalCatalog.GetGlobalCatalog(ServerContext);

						//compare objectGuid of replication metadata history record and actual server
						//required if server was restaged required historical sync state will not work
						String ServerGuidActual = FoundServer.GetDirectoryEntry().Guid.ToString();
						String ServerGuidHistorical = this.SynchronizationHistory.DomainSyncStates[ForestDomain.Name].ServerGuid;
						if (ServerGuidActual.Equals(ServerGuidHistorical, StringComparison.OrdinalIgnoreCase))
						{
							//test the server
							object BindServer = FoundServer.GetDirectoryEntry().NativeObject;

							this.SynchronizationHistory.DomainSyncStates[ForestDomain.Name].HighestCommittedUSN = FoundServer.HighestCommittedUsn;

							return this.SynchronizationHistory.DomainSyncStates[ForestDomain.Name];
						}
					}
					//server not found or not available
					catch
					{
						FoundServer = null;
					}
				}

				IEnumerator<GlobalCatalog> ServerEnumerator = this.GetPreferedGC(DirectoryContextType.Domain, ForestDomain.Name).GetEnumerator();

				while (ServerEnumerator.MoveNext() && FoundServer == null)
				{
					try
					{
						FoundServer = ServerEnumerator.Current;
						object BindServer = FoundServer.GetDirectoryEntry().NativeObject;
					}
					catch
					{
						FoundServer = null;
					}
				}
				return new SyncServer(
					FoundServer.Name,
					FoundServer.GetDirectoryEntry().Guid.ToString(),
					FoundServer.HighestCommittedUsn,
					Convert.ToInt64(FoundServer.HighestCommittedUsn * (1 - InitializationFactor)));
			}
			catch (Exception eX)
			{
				throw new Exception(string.Format("{0}::Unable to connect to any GC in {1} domain!", new StackFrame(0, true).GetMethod().Name, ForestDomain.Name, eX.Message));
			}
		}

		/// <summary>
		/// Method translates attribute into string collection
		/// Works for single and multi-valued attributes
		/// Single-valued attribute will result in one collection entry
		/// ISSpyra
		/// </summary>
		/// <param name="ObjectAttribute"></param>
		/// <returns></returns>
		private IEnumerable<String> TranslateAtribute(DirectoryAttribute ObjectAttribute)
		{
			Type AttributeType = ObjectAttribute[0].GetType();

			if (AttributeType.IsArray)
			{
				AttributeType = AttributeType.GetElementType();
			}

			if (Type.GetTypeCode(AttributeType) == TypeCode.Byte)
			{
				object[] AttributeValues = ObjectAttribute.GetValues(Type.GetType("System.Byte[]"));


				if (ObjectAttribute.Name.Equals("objectGuid", StringComparison.OrdinalIgnoreCase))
				{
					yield return (new Guid((byte[])AttributeValues[0])).ToString();
				}
				else
				{
					for (int i = 0; i < AttributeValues.Length; i++)
					{
						SecurityIdentifier ObjectSid = new SecurityIdentifier((byte[])AttributeValues[i], 0);
						if (ObjectSid.IsAccountSid())
						{
							yield return ObjectSid.Value;
						}
					}
				}
			}
			else
			{
				object[] AttributeValues = ObjectAttribute.GetValues(AttributeType);
				foreach (object SingleValue in AttributeValues)
				{
					yield return SingleValue.ToString();
				}
			}
		}

		/// <summary>
		/// Method translates attribute into string collection
		/// Works for single and multi-valued attributes
		/// Single-valued attribute will result in one collection entry
		/// ISSpyra
		/// </summary>
		/// <param name="ObjectAttribute"></param>
		/// <returns></returns>
		private IEnumerable<String> TranslateAtribute(Object[] AttributeValues, bool isObjectGuid)
		{
			Type AttributeType = AttributeValues[0].GetType();

			if (AttributeType.IsArray)
			{
				AttributeType = AttributeType.GetElementType();
			}
			if (Type.GetTypeCode(AttributeType) == TypeCode.Byte)
			{
				if (isObjectGuid)
				{
					yield return (new Guid((byte[])AttributeValues[0])).ToString();
				}
				else
				{
					for (int i = 0; i < AttributeValues.Length; i++)
					{
						SecurityIdentifier ObjectSid = new SecurityIdentifier((byte[])AttributeValues[i], 0);
						if (ObjectSid.IsAccountSid())
						{
							yield return ObjectSid.Value;
						}
					}
				}
			}
			else
			{
				foreach (object SingleValue in AttributeValues)
				{
					yield return SingleValue.ToString();
				}
			}
		}

		/// <summary>
		/// Method retrieves all changed legacy groups
		/// That were migrated under respective Global OU
		/// </summary>
		/// <param name="ObjectDistinguishedName"></param>
		/// <param name="AttributeName"></param>
		/// <returns></returns>
		private int LoadJobsLegacyBased(Domain ForestDomain, Ldap GlobalLdapObject, String GroupLdapFilter, String UserLdapFilter, String GroupBaseDn, double InitializationFactor)
		{
			try
			{
				int JobsLoaded = 0;

				SyncServer SynchronizationServer = this.GetPreferredGC(ForestDomain, InitializationFactor);

				using (Ldap LegacyLdapGCObject = new Ldap(SynchronizationServer.ServerFqdn, null, null, null, 3268))
				{
					String LegacyLdapFilter = String.Format(@"(&(uSNChanged>={0})({1}))",
						SynchronizationServer.HistoricallyHighestCommittedUSN,
						GroupLdapFilter
						);

					Parallel.ForEach(LegacyLdapGCObject.RetrieveAttributes(LegacyLdapFilter, new String[] { "objectSid", "distinguishedName" }, false), (SearchResultGroup) =>
					{
						try
						{
							IEnumerator<String> GroupAttributeValue = this.TranslateAtribute((Object[])SearchResultGroup["objectSid"], false).GetEnumerator();
							GroupAttributeValue.MoveNext();

							String GlobalGroupDn = this.GetGlobalDomainGroupDn(
								GlobalLdapObject,
								GroupAttributeValue.Current,
								GroupBaseDn,
								GroupLdapFilter
								);

							if (GlobalGroupDn != null)
							{
								String LegacyGroupDn = ((Object[])SearchResultGroup["distinguishedName"])[0].ToString();

								foreach (MembershipAction LegacyChanges in this.GetObjectDeltasOfValues(LegacyLdapGCObject.LdapConnectionObject, LegacyGroupDn, SynchronizationServer.HistoricallyHighestCommittedUSN))
								{
									String LegacyUserSamID = null;

									//filters double check if synchronization metadata change is still valid
									//and whether users still is/is not a DIRECT member of specific group
									if (LegacyChanges.Action == MembershipAction.Operation.ADD)
									{
										String LegacyUserFilter = String.Format("(&(memberof={0}){1})", LegacyGroupDn, UserLdapFilter);

										LegacyUserSamID = this.GetLegacyDomainUserSamID(
										LegacyLdapGCObject,
										LegacyUserFilter,
										LegacyChanges.MemberDistinguishedName);
									}
									else
									{
										String LegacyUserFilter = String.Format("(&(!memberof={0}){1})", LegacyGroupDn, UserLdapFilter);

										LegacyUserSamID = this.GetLegacyDomainUserSamID(
											LegacyLdapGCObject,
											LegacyUserFilter,
											LegacyChanges.MemberDistinguishedName);

										if (LegacyUserSamID != null)
										{
											String LegacyForestUserFilter = String.Format("(&(sAMAccountName={0})(memberof={1}){2})", LegacyUserSamID, LegacyGroupDn, USER_CLASS_FILTER);

											String UserObjectForestSamID = this.GetLegacyForestUserSamID(
											LegacyLdapGCObject,
											LegacyForestUserFilter,
											LegacyChanges.MemberDistinguishedName);

											if (UserObjectForestSamID != null)
											{
												LegacyUserSamID = null;
											}
										}
									}

									if (LegacyUserSamID != null)
									{
										//filters double check if synchronization metadata change is still valid
										//and whether global users still is not an INDIRECT member of specific group for ADD operation
										//and whether global users still is a DIRECT member of specific group for REM operation
										String GlobalUserFilter = (LegacyChanges.Action == MembershipAction.Operation.ADD)
											? String.Format("(&(!memberof:1.2.840.113556.1.4.1941:={0}){1})", GlobalGroupDn, USER_CLASS_FILTER)
											: String.Format("(&(memberof={0}){1})", GlobalGroupDn, USER_CLASS_FILTER);

										String GlobalUserDn = this.GetGlobalDomainUserDn(
											GlobalLdapObject,
											LegacyUserSamID,
											null,
											GlobalUserFilter
											);

										if (GlobalUserDn != null)
										{
											JobsLoaded++;
											this.JobsToExecute.AddJob(GlobalGroupDn, new MembershipAction(GlobalUserDn, LegacyChanges.Action));
										}
									}
								}
							}
						}
						catch (Exception eX)
						{
							this.LogManager.WriteLog(new iLogEntry(String.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message), 3052, iLogEntry.LogEntrySeverity.Error));
						}
					});

					this.SynchronizationHistory.UpdateSyncHistory(ForestDomain.Name, SynchronizationServer);
				}
				return JobsLoaded;
			}
			catch (Exception eX)
			{
				throw new Exception(string.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message));
			}
		}

		/// <summary>
		/// Returns Global group for given legacy objectSid
		/// Search through Global groups sidHistory
		/// </summary>
		/// <param name="LdapConnection"></param>
		/// <param name="ObjectSid"></param>
		/// <param name="BaseDN"></param>
		/// <param name="LdapFilter"></param>
		/// <returns></returns>
		private String GetGlobalDomainGroupDn(Ldap LdapConnection, String ObjectSid, String BaseDN, String LdapFilter)
		{
			String GroupDistinguishedName = null;
			try
			{
				String[] AttributesToLoad = new String[] { "distinguishedName" };
				LdapFilter = String.Format("(&(sidHistory={0}){1})", ObjectSid, LdapFilter);

				foreach( Dictionary<String, Object> GroupResult in LdapConnection.RetrieveAttributes(LdapFilter, AttributesToLoad, BaseDN, false) )
				{
					GroupDistinguishedName = ((Object[])GroupResult["distinguishedName"])[0].ToString();
				}
			}
			catch (Exception eX)
			{
				this.LogManager.WriteLog(new iLogEntry(String.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message), 3053, iLogEntry.LogEntrySeverity.Warning));
			}
			return GroupDistinguishedName;
		}

		/// <summary>
		/// Returns Global user for given legacy objectSid
		/// Search through Global users sidHistory
		/// </summary>
		/// <param name="LdapConnection"></param>
		/// <param name="SamAccountName"></param>
		/// <param name="BaseDN"></param>
		/// <param name="LdapFilter"></param>
		/// <returns></returns>
		private String GetGlobalDomainUserDn(Ldap LdapConnection, String SamAccountName, String BaseDN, String LdapFilter)
		{
			String UserDistinguishedName = null;
			try
			{
				String[] AttributesToLoad = new String[] { "distinguishedName" };
				LdapFilter = String.Format("(&(sAMAccountName={0}){1})", SamAccountName, LdapFilter);

				foreach(Dictionary<String, Object> UserResult in LdapConnection.RetrieveAttributes(LdapFilter, AttributesToLoad,BaseDN, false) )
				{
					UserDistinguishedName = ((Object[])UserResult["distinguishedName"])[0].ToString();
				}
			}
			catch (Exception eX)
			{
				this.LogManager.WriteLog(new iLogEntry(String.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message), 3054, iLogEntry.LogEntrySeverity.Warning));
			}
			return UserDistinguishedName;
		}


		/// <summary>
		/// Returns Legacy user for given legacy dn
		/// </summary>
		/// <param name="LdapConnection"></param>
		/// <param name="ObjectSid"></param>
		/// <param name="BaseDN"></param>
		/// <param name="LdapFilter"></param>
		/// <returns></returns>
		private String GetLegacyForestUserSamID(Ldap LdapConnection, String LdapFilter, String BaseDN)
		{
			String UserObjectSamID = null;
			try
			{
				String ForestDN = this.GetDomainContextDN(this.LegacyForest);

				String[] AttributesToLoad = new String[] { "sAMAccountName" };
				foreach (Dictionary<String, Object> UserResult in LdapConnection.RetrieveAttributes(LdapFilter, AttributesToLoad, ForestDN, false))
				{
					foreach (String AttributeValue in this.TranslateAtribute((Object[])UserResult["sAMAccountName"], false))
					{
						UserObjectSamID = AttributeValue;
					}
				}
			}
			catch (Exception eX)
			{
				this.LogManager.WriteLog(new iLogEntry(String.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message), 3055, iLogEntry.LogEntrySeverity.Warning));
			}
			return UserObjectSamID;
		}

		/// <summary>
		/// Returns Legacy user for given legacy dn
		/// </summary>
		/// <param name="LdapConnection"></param>
		/// <param name="ObjectSid"></param>
		/// <param name="BaseDN"></param>
		/// <param name="LdapFilter"></param>
		/// <returns></returns>
		private String GetLegacyDomainUserSamID(Ldap LdapConnection, String LdapFilter, String BaseDN)
		{
			String UserObjectSamID = null;
			try
			{
				String[] AttributesToLoad = new String[] { "sAMAccountName" };

				foreach (Dictionary<String, Object> UserResult in LdapConnection.RetrieveAttributes(LdapFilter, AttributesToLoad, BaseDN, false))
				{
					foreach (String AttributeValue in this.TranslateAtribute((Object[])UserResult["sAMAccountName"], false))
					{
						UserObjectSamID = AttributeValue;
					}
				}
			}
			catch (Exception eX)
			{
				this.LogManager.WriteLog(new iLogEntry(String.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message), 3055, iLogEntry.LogEntrySeverity.Warning));
			}
			return UserObjectSamID;
		}
	
		/// <summary>
		/// Method retrieves GC servers from list of servers provided
		/// </summary>
		/// <param name="DirectoryContextType"></param>
		/// <param name="PreferedServers"></param>
		/// <returns></returns>
		private IEnumerable<GlobalCatalog> GetPreferedGC(String[] PreferedServers)
		{
			foreach (String ServerFqdn in PreferedServers)
			{
				foreach (IPAddress ServerIPAddress in Dns.GetHostEntry(ServerFqdn).AddressList)
				{
					DirectoryContext ServerContext = new DirectoryContext(DirectoryContextType.DirectoryServer, ServerIPAddress.ToString());
					yield return GlobalCatalog.FindOne(ServerContext);
				}
			}
		}

		/// <summary>
		/// Method retrieves GC servers from forest
		/// </summary>
		/// <param name="PreferedServers"></param>
		/// <returns></returns>
		private IEnumerable<GlobalCatalog>	GetPreferedGC(DirectoryContextType ContextType, String ContextFqdn)
		{
			DirectoryContext Context = new DirectoryContext(ContextType, ContextFqdn);
			if (ContextType == DirectoryContextType.Domain)
			{
				Domain FoundDomain = Domain.GetDomain(Context);
				foreach (DomainController DCServer in FoundDomain.DomainControllers)
				{
					DirectoryContext ServerContext = new DirectoryContext(DirectoryContextType.DirectoryServer, DCServer.Name);

					if (DCServer.IsGlobalCatalog()) yield return GlobalCatalog.GetGlobalCatalog(ServerContext);
				}
			}
			else if (ContextType == DirectoryContextType.Forest)
			{
				foreach (GlobalCatalog GCServer in GlobalCatalog.FindAll(Context))
				{
					yield return GCServer;
				}
			}
		}

		/// <summary>
		/// Retrieves MS AD(DS) object linking changes.
		/// Linking attribute is casted here into LinkingUpdate object.
		/// ISSpyra
		/// </summary>
		private IEnumerable<MembershipAction> GetObjectDeltasOfValues(LdapConnection oLdapConnection, string BaseDN, long iHighestCommittedUSN)
		{
			const ushort RETRY_NO = 7;
			const String MEMBER_ATTRIBUTE = "member";

			SearchResponse dirRes = null;
			SearchRequest srRequest = null;
			SearchResultEntry srEntry = null;

			string sRange = String.Format(@"{0};range={{0}}-{{1}}", SYNC_METADATA_VALUE);

			int iIndex = 0;
			int iStep = 0;

			string sCurrentRange = String.Format(sRange, iIndex, '*');
			bool bMoreData = true;
			while (bMoreData)
			{

				ushort iTries = 0;
				bool bRetry = false;
				do
				{
					try
					{
						iTries++;

						srRequest = new SearchRequest(
								BaseDN,
								@"(objectClass=*)",
								System.DirectoryServices.Protocols.SearchScope.Base,
								new string[] { sCurrentRange }
								);

						dirRes = (SearchResponse)oLdapConnection.SendRequest(srRequest);
						bRetry = false;
					}
					catch (DirectoryOperationException ex)
					{
						if (iTries % RETRY_NO == 0)
						{
							bRetry = false;
							throw new Exception(string.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, ex.Message));
						}
						else
						{
							bRetry = true;
						}
					}
				} while (bRetry);

				if (dirRes.Entries.Count > 0 && dirRes.Entries[0].Attributes.Count > 0)
				{
					srEntry = dirRes.Entries[0];
					foreach (string sAttr in srEntry.Attributes.AttributeNames)
					{
						foreach (object oValue in srEntry.Attributes[sAttr].GetValues(Type.GetType("System.String")))
						{
							Dictionary<string, string> dicAttributeReplData = GetXMLNodeInnertext(oValue.ToString(), new string[] { "pszAttributeName", "pszObjectDn", "ftimeDeleted", "usnLocalChange" }, true);
							if (dicAttributeReplData["pszAttributeName"].Equals(MEMBER_ATTRIBUTE, StringComparison.OrdinalIgnoreCase))
							{
								string sLocalChangeUSN = null;
								if (dicAttributeReplData.TryGetValue("usnLocalChange", out sLocalChangeUSN))
								{
									if (Convert.ToInt64(dicAttributeReplData["usnLocalChange"]) > iHighestCommittedUSN)
									{
										if (dicAttributeReplData["ftimeDeleted"].Equals("1601-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase))
										{
											yield return new MembershipAction(dicAttributeReplData["pszObjectDn"], MembershipAction.Operation.ADD);
										}
										else
										{
											yield return new MembershipAction(dicAttributeReplData["pszObjectDn"], MembershipAction.Operation.REM);
										}
									}
								}
							}
							iIndex++;
						}

						if (sAttr.IndexOf('*') > 0)
						{
							bMoreData = false;
						}
						else
						{
							iStep = srEntry.Attributes[sAttr].Count;
							sCurrentRange = String.Format(sRange, iIndex, iIndex + iStep);
						}
					}
				}
				else
				{
					bMoreData = false;
				}
			}
		}

		/// <summary>
		/// Commits membership change to the group
		/// </summary>
		/// <param name="LdapConnectionObject"></param>
		/// <param name="GroupDistinguishedName"></param>
		/// <param name="MemberDistinguishedName"></param>
		/// <param name="IsRemoval"></param>
		private void CommitMembershipChange(LdapConnection LdapConnectionObject, String GroupDistinguishedName, String MemberDistinguishedName, bool IsRemoval, bool Commit)
		{
			try
			{
				const String ATTR_NAME = "member";

				ModifyRequest ModificationRequest = new ModifyRequest(
					GroupDistinguishedName,
					IsRemoval ? DirectoryAttributeOperation.Delete : DirectoryAttributeOperation.Add,
					ATTR_NAME,
					new String[] { MemberDistinguishedName });

				PermissiveModifyControl PermissiveModify = new PermissiveModifyControl();

				ModificationRequest.Controls.Add(PermissiveModify);

				if (Commit)
				{
					ModifyResponse ModificationResponse = (ModifyResponse)LdapConnectionObject.SendRequest(ModificationRequest);
				}
			}
			catch (DirectoryOperationException eX)
			{
				throw new Exception(string.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, eX.Message));
			}
		}

		/// <summary>
		/// Retrieves key/value collection from XML String
		/// Filters only node names specified in aNodeName param
		/// ISSpyra
		/// </summary>
		/// <param name="sXmlNodes"></param>
		/// <param name="aNodeNames"></param>
		/// <param name="bOrderedUnique"></param>
		/// <returns></returns>
		private Dictionary<string, string> GetXMLNodeInnertext(string sXmlNodes, string[] aNodeNames, bool bOrderedUnique)
		{
			try
			{
				Dictionary<string, string> dicRes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

				if (bOrderedUnique)
				{
					int iStart = -1;
					int iFinish = -1;

					foreach (string sNode in aNodeNames)
					{
						if (sXmlNodes.Length > iFinish)
						{
							iStart = sXmlNodes.IndexOf('<' + sNode + '>', iFinish + 1);

							if (iStart >= 0)
							{
								iStart += (sNode.Length + 2);
								iFinish = sXmlNodes.IndexOf("</" + sNode + '>', iStart);

								dicRes.Add(sNode, sXmlNodes.Substring(iStart, iFinish - iStart));
							}
						}
					}
				}
				else
				{
					foreach (string sNode in aNodeNames)
					{
						int iStart = -1;
						int iFinish = -1;

						iStart = sXmlNodes.IndexOf('<' + sNode + '>', 0);

						if (iStart >= 0)
						{
							iStart += (sNode.Length + 2);
							iFinish = sXmlNodes.IndexOf("</" + sNode + '>', iStart);

							dicRes.Add(sNode, sXmlNodes.Substring(iStart, iFinish - iStart));
						}
					}
				}
				return dicRes;
			}
			catch (Exception ex)
			{
				throw new Exception(string.Format("{0}::{1}", new StackFrame(0, true).GetMethod().Name, ex.Message));
			}
		}
		#endregion

		/// <summary>
		/// Function converts Domain Context FQDN to Domain DistinguishedName
		/// </summary>
		private String GetDomainContextDN(String DomainContextFQDN)
		{
			return String.Format("DC={0}", DomainContextFQDN.Replace(".",",DC="));
		}

		#region enum
		public enum ExecutionModes
		{
			NoCommit = 0x01,
			FullSync = 0x02
		}
		#endregion

		#region IDisposable Members
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool bDisposing)
		{
			if (!this.bDisposed)
			{
				if (bDisposing)
				{
					this.LogManager.Dispose();
				}

				this.bDisposed = true;
			}
		}

		#endregion
	}
}